<?php
/**
 * library/FeeSheet.class.php
 *
 * Base class for implementations of the Fee Sheet.
 * This should not include UI but may be extended by a class that does.
 *
 * LICENSE: This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see
 * http://www.gnu.org/licenses/licenses.html#GPL .
 *
 * @package OpenEMR
 * @license http://www.gnu.org/licenses/licenses.html#GPL GNU GPL V3+
 * @author  Rod Roark <rod@sunsetsystems.com>
 * @link    http://www.open-emr.org
 */





require_once(dirname(__FILE__) . "/../interface/globals.php");
require_once(dirname(__FILE__) . "/acl.inc");
require_once(dirname(__FILE__) . "/../custom/code_types.inc.php");
require_once(dirname(__FILE__) . "/../interface/drugs/drugs.inc.php");
require_once(dirname(__FILE__) . "/options.inc.php");
require_once(dirname(__FILE__) . "/appointment_status.inc.php");
require_once(dirname(__FILE__) . "/classes/Prescription.class.php");
require_once(dirname(__FILE__) . "/forms.inc");
require_once(dirname(__FILE__) . "/log.inc");
// For logging checksums set this to true.
define('CHECKSUM_LOGGING', true);

// require_once(dirname(__FILE__) . "/api.inc");
// require_once(dirname(__FILE__) . "/forms.inc");

class FeeSheet
{

    public $pid;                                // patient id
    public $encounter;                          // encounter id
    public $got_warehouses = false;             // if there is more than 1 warehouse
    public $default_warehouse = '';             // logged-in user's default warehouse
    public $visit_date = '';                    // YYYY-MM-DD date of this visit
    public $match_services_to_products = false; // For IPPF
    public $patient_age = 0;                    // Age in years as of the visit date
    public $patient_male = 0;                   // 1 if male
    public $patient_pricelevel = '';            // From patient_data.pricelevel
    public $provider_id = 0;
    public $supervisor_id = 0;
    public $code_is_in_fee_sheet = false;       // Set by genCodeSelectorValue()

  // Possible units of measure for NDC drug quantities.
    public $ndc_uom_choices = array(
    'ML' => 'ML',
    'GR' => 'Grams',
    'ME' => 'Milligrams',
    'F2' => 'I.U.',
    'UN' => 'Units'
    );

  // Set by checkRelatedForContraception():
    public $line_contra_code     = '';
    public $line_contra_cyp      = 0;
    public $line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult

  // Array of line items generated by addServiceLineItem().
  // Each element is an array of line item attributes.
    public $serviceitems = array();

  // Array of line items generated by addProductLineItem().
  // Each element is an array of line item attributes.
    public $productitems = array();

  // Indicates if any line item has a fee.
    public $hasCharges = false;

  // Indicates if any clinical services or products are in the fee sheet.
    public $required_code_count = 0;

  // These variables are used to compute the initial consult service with highest CYP (IPPF).
    public $contraception_code = '';
    public $contraception_cyp  = 0;

    public $ALLOW_COPAYS = false;

    function __construct($pid = 0, $encounter = 0)
    {
        if (empty($pid)) {
            $pid = $GLOBALS['pid'];
        }

        if (empty($encounter)) {
            $encounter = $GLOBALS['encounter'];
        }

        $this->pid = $pid;
        $this->encounter = $encounter;

        // IPPF doesn't want any payments to be made or displayed in the Fee Sheet.
        $this->ALLOW_COPAYS = !$GLOBALS['ippf_specific'];

        // Get the user's default warehouse and an indicator if there's a choice of warehouses.
        $wrow = sqlQuery("SELECT count(*) AS count FROM list_options WHERE list_id = 'warehouse' AND activity = 1");
        $this->got_warehouses = $wrow['count'] > 1;
        $wrow = sqlQuery(
            "SELECT default_warehouse FROM users WHERE username = ?",
            array($_SESSION['authUser'])
        );
        $this->default_warehouse = empty($wrow['default_warehouse']) ? '' : $wrow['default_warehouse'];

        // Get some info about this visit.
        $visit_row = sqlQuery("SELECT fe.date, fe.provider_id, fe.supervisor_id, " .
          "opc.pc_catname, fac.extra_validation " .
          "FROM form_encounter AS fe " .
          "LEFT JOIN openemr_postcalendar_categories AS opc ON opc.pc_catid = fe.pc_catid " .
          "LEFT JOIN facility AS fac ON fac.id = fe.facility_id " .
          "WHERE fe.pid = ? AND fe.encounter = ? LIMIT 1", array($this->pid, $this->encounter));
        $this->visit_date    = substr($visit_row['date'], 0, 10);
        $this->provider_id   = $visit_row['provider_id'];
        if (empty($this->provider_id)) {
            $this->provider_id = $this->findProvider();
        }

        $this->supervisor_id = $visit_row['supervisor_id'];
        // This flag is specific to IPPF validation at form submit time.  It indicates
        // that most contraceptive services and products should match up on the fee sheet.
        $this->match_services_to_products = $GLOBALS['ippf_specific'] &&
          !empty($visit_row['extra_validation']);

        // Get some information about the patient.
        $patientrow = getPatientData($this->pid, "DOB, sex, pricelevel");
        $this->patient_age = $this->getAge($patientrow['DOB'], $this->visit_date);
        $this->patient_male = strtoupper(substr($patientrow['sex'], 0, 1)) == 'M' ? 1 : 0;
        $this->patient_pricelevel = $patientrow['pricelevel'];
    }

  // Convert numeric code type to the alpha version.
  //
    public static function alphaCodeType($id)
    {
        global $code_types;
        foreach ($code_types as $key => $value) {
            if ($value['id'] == $id) {
                return $key;
            }
        }

        return '';
    }

  // Compute age in years given a DOB and "as of" date.
  //
    public static function getAge($dob, $asof = '')
    {
        if (empty($asof)) {
            $asof = date('Y-m-d');
        }

        $a1 = explode('-', substr($dob, 0, 10));
        $a2 = explode('-', substr($asof, 0, 10));
        $age = $a2[0] - $a1[0];
        if ($a2[1] < $a1[1] || ($a2[1] == $a1[1] && $a2[2] < $a1[2])) {
            --$age;
        }

        return $age;
    }

  // Gets the provider from the encounter, logged-in user or patient demographics.
  // Adapted from work by Terry Hill.
  //
    public function findProvider()
    {
        $find_provider = sqlQuery(
            "SELECT provider_id FROM form_encounter " .
            "WHERE pid = ? AND encounter = ? ORDER BY id DESC LIMIT 1",
            array($this->pid, $this->encounter)
        );
        $providerid = $find_provider['provider_id'];
        if (!$providerid) {
            $get_authorized = $_SESSION['userauthorized'];
            if ($get_authorized == 1) {
                $providerid = $_SESSION['authUserID'];
            }
        }

        if (!$providerid) {
            $find_provider = sqlQuery("SELECT providerID FROM patient_data " .
            "WHERE pid = ?", array($this->pid));
            $providerid = $find_provider['providerID'];
        }

        return intval($providerid);
    }

  // Log a message that is easy for the Re-Opened Visits Report to interpret.
  //
    public function logFSMessage($action)
    {
        newEvent(
            'fee-sheet',
            $_SESSION['authUser'],
            $_SESSION['authProvider'],
            1,
            $action,
            $this->pid,
            $this->encounter
        );
    }

  // Compute a current checksum of this encounter's Fee Sheet data from the database.
  //
    public function visitChecksum($saved = false)
    {
        $rowb = sqlQuery(
            "SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
            "id, code, modifier, units, fee, authorized, provider_id, ndc_info, justify, billed" .
            "))) AS checksum FROM billing WHERE " .
            "pid = ? AND encounter = ? AND activity = 1",
            array($this->pid, $this->encounter)
        );
        $rowp = sqlQuery(
            "SELECT BIT_XOR(CRC32(CONCAT_WS(',', " .
            "sale_id, inventory_id, prescription_id, quantity, fee, sale_date, billed" .
            "))) AS checksum FROM drug_sales WHERE " .
            "pid = ? AND encounter = ?",
            array($this->pid, $this->encounter)
        );
        $ret = intval($rowb['checksum']) ^ intval($rowp['checksum']);
        if (CHECKSUM_LOGGING) {
            $comment = "Checksum = '$ret'";
            $comment .= ", Saved = " . ($saved ? "true" : "false");
            newEvent("checksum", $_SESSION['authUser'], $_SESSION['authProvider'], 1, $comment, $this->pid);
        }

        return $ret;
    }

  // IPPF-specific; get contraception attributes of the related codes.
  //
    public function checkRelatedForContraception($related_code, $is_initial_consult = false)
    {
        $this->line_contra_code     = '';
        $this->line_contra_cyp      = 0;
        $this->line_contra_methtype = 0; // 0 = None, 1 = Not initial, 2 = Initial consult
        if (!empty($related_code)) {
            $relcodes = explode(';', $related_code);
            foreach ($relcodes as $relstring) {
                if ($relstring === '') {
                    continue;
                }

                list($reltype, $relcode) = explode(':', $relstring);
                if ($reltype !== 'IPPFCM') {
                    continue;
                }

                $methtype = $is_initial_consult ? 2 : 1;
                $tmprow = sqlQuery("SELECT cyp_factor FROM codes WHERE " .
                "code_type = '32' AND code = ? LIMIT 1", array($relcode));
                $cyp = 0 + $tmprow['cyp_factor'];
                if ($cyp > $this->line_contra_cyp) {
                        $this->line_contra_cyp      = $cyp;
                        // Note this is an IPPFCM code, not an IPPF2 code.
                        $this->line_contra_code     = $relcode;
                        $this->line_contra_methtype = $methtype;
                }
            }
        }
    }

  // Insert a row into the lbf_data table. Returns a new form ID if that is not provided.
  // This is only needed for auto-creating Contraception forms.
  //
    public function insert_lbf_item($form_id, $field_id, $field_value)
    {
        if ($form_id) {
            sqlInsert("INSERT INTO lbf_data (form_id, field_id, field_value) " .
            "VALUES (?, ?, ?)", array($form_id, $field_id, $field_value));
        } else {
            $form_id = sqlInsert("INSERT INTO lbf_data (field_id, field_value) " .
            "VALUES (?, ?)", array($field_id, $field_value));
        }

        return $form_id;
    }

  // Create an array of data for a particular billing table item that is useful
  // for building a user interface form row.  $args is an array containing:
  //  codetype
  //  code
  //  modifier
  //  ndc_info
  //  auth
  //  del
  //  units
  //  fee
  //  id
  //  billed
  //  code_text
  //  justify
  //  provider_id
  //  notecodes
  //  pricelevel
    public function addServiceLineItem($args)
    {
        global $code_types;

        // echo "<!-- \n"; // debugging
        // print_r($args); // debugging
        // echo "--> \n";  // debugging

        $li = array();
        $li['hidden'] = array();

        $codetype    = $args['codetype'];
        $code        = $args['code'];
        $revenue_code    = isset($args['revenue_code']) ? $args['revenue_code'] : '';
        $modifier    = isset($args['modifier']) ? $args['modifier'] : '';
        $code_text   = isset($args['code_text']) ? $args['code_text'] : '';
        $units       = isset($args['units']) ? $args['units'] : 0;
        $units       = max(1, intval($units));
        $billed      = !empty($args['billed']);
        $auth        = !empty($args['auth']);
        $id          = isset($args['id']) ? intval($args['id']) : 0;
        $ndc_info    = isset($args['ndc_info']) ? $args['ndc_info'] : '';
        $provider_id = isset($args['provider_id']) ? intval($args['provider_id']) : 0;
        $justify     = isset($args['justify']) ? $args['justify'] : '';
        $notecodes   = isset($args['notecodes']) ? $args['notecodes'] : '';
        $fee         = isset($args['fee']) ? (0 + $args['fee']) : 0;
        // Price level should be unset only if adding a new line item.
        $pricelevel  = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel;
        $del         = !empty($args['del']);

        // If using line item billing and user wishes to default to a selected provider, then do so.
        if (!empty($GLOBALS['default_fee_sheet_line_item_provider']) && !empty($GLOBALS['support_fee_sheet_line_item_provider'])) {
            if ($provider_id == 0) {
                $provider_id = 0 + $this->findProvider();
            }
        }

        if ($codetype == 'COPAY') {
            if (!$code_text) {
                $code_text = 'Cash';
            }

            if ($fee > 0) {
                $fee = 0 - $fee;
            }
        }

        // Get the matching entry from the codes table.
        $sqlArray = array();
        $query = "SELECT id, units, code_text, revenue_code FROM codes WHERE " .
        "code_type = ? AND code = ?";
        array_push($sqlArray, $code_types[$codetype]['id'], $code);
        if ($modifier) {
            $query .= " AND modifier = ?";
            array_push($sqlArray, $modifier);
        } else {
            $query .= " AND (modifier IS NULL OR modifier = '')";
        }

        $result = sqlQuery($query, $sqlArray);
        $codes_id = $result['id'];
        $revenue_code = $revenue_code ? $revenue_code : $result['revenue_code'];
        if (!$code_text) {
            $code_text = $result['code_text'];
            if (empty($units)) {
                $units = max(1, intval($result['units']));
            }

            if (!isset($args['fee'])) {
                // Fees come from the prices table now.
                $query = "SELECT pr_price FROM prices WHERE " .
                "pr_id = ? AND pr_selector = '' AND pr_level = ? " .
                "LIMIT 1";
                // echo "\n<!-- $query -->\n"; // debugging
                $prrow = sqlQuery($query, array($codes_id, $pricelevel));
                $fee = empty($prrow) ? 0 : $prrow['pr_price'];
            }
        }

        $fee = sprintf('%01.2f', $fee);

        $li['hidden']['code_type'] = $codetype;
        $li['hidden']['code'] = $code;
        $li['hidden']['revenue_code'] = $revenue_code;
        $li['hidden']['mod'] = $modifier;
        $li['hidden']['billed'] = $billed;
        $li['hidden']['id'] = $id;
        $li['hidden']['codes_id'] = $codes_id;

        // This logic is only used for family planning clinics, and then only when
        // the option is chosen to use or auto-generate Contraception forms.
        // It adds contraceptive method and effectiveness to relevant lines.
        if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy'] && $codetype == 'MA') {
            $codesrow = sqlQuery(
                "SELECT related_code, cyp_factor FROM codes WHERE " .
                "code_type = ? AND code = ? LIMIT 1",
                array($code_types[$codetype]['id'], $code)
            );
            $this->checkRelatedForContraception($codesrow['related_code'], $codesrow['cyp_factor']);
            if ($this->line_contra_code) {
                  $li['hidden']['method'  ] = $this->line_contra_code;
                  $li['hidden']['cyp'     ] = $this->line_contra_cyp;
                  $li['hidden']['methtype'] = $this->line_contra_methtype;
                  // contraception_code is only concerned with initial consults.
                if ($this->line_contra_cyp > $this->contraception_cyp && $this->line_contra_methtype == 2) {
                    $this->contraception_cyp  = $this->line_contra_cyp;
                    $this->contraception_code = $this->line_contra_code;
                }
            }
        }

        if ($codetype == 'COPAY') {
            $li['codetype'] = xl($codetype);
            if ($ndc_info) {
                $li['codetype'] .= " ($ndc_info)";
            }

            $ndc_info = '';
        } else {
            $li['codetype'] = $codetype;
        }

        $li['code'     ] = $codetype == 'COPAY' ? '' : $code;
        $li['revenue_code'      ] = $revenue_code;
        $li['mod'      ] = $modifier;
        $li['fee'      ] = $fee;
        $li['price'    ] = $fee / $units;
        $li['pricelevel'] = $pricelevel;
        $li['units'    ] = $units;
        $li['provid'   ] = $provider_id;
        $li['justify'  ] = $justify;
        $li['notecodes'] = $notecodes;
        $li['del'      ] = $id && $del;
        $li['code_text'] = $code_text;
        $li['auth'     ] = $auth;

        $li['hidden']['price'] = $li['price'];

        // If NDC info exists or may be required, add stuff for it.
        if ($codetype == 'HCPCS' && !$billed) {
            $ndcnum = '';
            $ndcuom = '';
            $ndcqty = '';
            if (preg_match('/^N4(\S+)\s+(\S\S)(.*)/', $ndc_info, $tmp)) {
                $ndcnum = $tmp[1];
                $ndcuom = $tmp[2];
                $ndcqty = $tmp[3];
            }

            $li['ndcnum'  ] = $ndcnum;
            $li['ndcuom'  ] = $ndcuom;
            $li['ndcqty'  ] = $ndcqty;
        } else if ($ndc_info) {
            $li['ndc_info'  ] = $ndc_info;
        }

        // For Family Planning.
        if ($codetype == 'MA') {
            ++$this->required_code_count;
        }

        if ($fee != 0) {
            $this->hasCharges = true;
        }

        $this->serviceitems[] = $li;
    }

  // Create an array of data for a particular drug_sales table item that is useful
  // for building a user interface form row.  $args is an array containing:
  //  drug_id
  //  selector
  //  sale_id
  //  rx (boolean)
  //  del (boolean)
  //  units
  //  fee
  //  billed
  //  warehouse_id
  //  pricelevel
  //
    public function addProductLineItem($args)
    {
        global $code_types;

        $li = array();
        $li['hidden'] = array();

        $drug_id      = $args['drug_id'];
        $selector     = isset($args['selector']) ? $args['selector'] : '';
        $sale_id      = isset($args['sale_id']) ? intval($args['sale_id']) : 0;
        $units        = isset($args['units']) ? $args['units'] : 0;
        $units        = max(1, intval($units));
        $billed       = !empty($args['billed']);
        $rx           = !empty($args['rx']);
        $del          = !empty($args['del']);
        $fee          = isset($args['fee']) ? (0 + $args['fee']) : 0;
        $pricelevel   = isset($args['pricelevel']) ? $args['pricelevel'] : $this->patient_pricelevel;
        $warehouse_id = isset($args['warehouse_id']) ? $args['warehouse_id'] : '';

        $drow = sqlQuery("SELECT name, related_code FROM drugs WHERE drug_id = ?", array($drug_id));
        $code_text = $drow['name'];

        // If no warehouse ID passed, use the logged-in user's default.
        if ($this->got_warehouses && $warehouse_id === '') {
            $warehouse_id = $this->default_warehouse;
        }

        // If fee is not provided, get it from the prices table.
        // It is assumed in this case that units will match what is in the product template.
        if (!isset($args['fee'])) {
            $query = "SELECT pr_price FROM prices WHERE " .
            "pr_id = ? AND pr_selector = ? AND pr_level = ? " .
            "LIMIT 1";
            $prrow = sqlQuery($query, array($drug_id, $selector, $pricelevel));
            $fee = empty($prrow) ? 0 : $prrow['pr_price'];
        }

        $fee = sprintf('%01.2f', $fee);

        $li['fee'      ] = $fee;
        $li['price'    ] = $fee / $units;
        $li['pricelevel'] = $pricelevel;
        $li['units'    ] = $units;
        $li['del'      ] = $sale_id && $del;
        $li['code_text'] = $code_text;
        $li['warehouse'] = $warehouse_id;
        $li['rx'       ] = $rx;

        $li['hidden']['drug_id'] = $drug_id;
        $li['hidden']['selector'] = $selector;
        $li['hidden']['sale_id'] = $sale_id;
        $li['hidden']['billed' ] = $billed;
        $li['hidden']['price'  ] = $li['price'];

        // This logic is only used for family planning clinics, and then only when
        // the option is chosen to use or auto-generate Contraception forms.
        // It adds contraceptive method and effectiveness to relevant lines.
        if ($GLOBALS['ippf_specific'] && $GLOBALS['gbl_new_acceptor_policy']) {
            $this->checkRelatedForContraception($drow['related_code']);
            if ($this->line_contra_code) {
                $li['hidden']['method'  ] = $this->line_contra_code;
                $li['hidden']['methtype'] = $this->line_contra_methtype;
            }
        }

        // For Family Planning.
        ++$this->required_code_count;
        if ($fee != 0) {
            $this->hasCharges = true;
        }

        $this->productitems[] = $li;
    }

  // Generate rows for items already in the billing table for this encounter.
  //
    public function loadServiceItems()
    {
        $billresult = getBillingByEncounter($this->pid, $this->encounter, "*");
        if ($billresult) {
            foreach ($billresult as $iter) {
                if (!$this->ALLOW_COPAYS && $iter["code_type"] == 'COPAY') {
                    continue;
                }

                $justify    = trim($iter['justify']);
                if ($justify) {
                    $justify = substr(str_replace(':', ',', $justify), 0, strlen($justify) - 1);
                }

                $this->addServiceLineItem(array(
                'id'          => $iter['id'],
                'codetype'    => $iter['code_type'],
                'code'        => trim($iter['code']),
                'revenue_code'    => trim($iter["revenue_code"]),
                'modifier'    => trim($iter["modifier"]),
                'code_text'   => trim($iter['code_text']),
                'units'       => $iter['units'],
                'fee'         => $iter['fee'],
                'pricelevel'  => $iter['pricelevel'],
                'billed'      => $iter['billed'],
                'ndc_info'    => $iter['ndc_info'],
                'provider_id' => $iter['provider_id'],
                'justify'     => $justify,
                'notecodes'   => trim($iter['notecodes']),
                ));
            }
        }

        // echo "<!-- \n"; // debugging
        // print_r($this->serviceitems); // debugging
        // echo "--> \n";  // debugging
    }

  // Generate rows for items already in the drug_sales table for this encounter.
  //
    public function loadProductItems()
    {
        $query = "SELECT ds.*, di.warehouse_id FROM drug_sales AS ds, drug_inventory AS di WHERE " .
        "ds.pid = ? AND ds.encounter = ?  AND di.inventory_id = ds.inventory_id " .
        "ORDER BY ds.sale_id";
        $sres = sqlStatement($query, array($this->pid, $this->encounter));
        while ($srow = sqlFetchArray($sres)) {
            $this->addProductLineItem(array(
            'drug_id'      => $srow['drug_id'],
            'selector'     => $srow['selector'],
            'sale_id'      => $srow['sale_id'],
            'rx'           => !empty($srow['prescription_id']),
            'units'        => $srow['quantity'],
            'fee'          => $srow['fee'],
            'pricelevel'   => $srow['pricelevel'],
            'billed'       => $srow['billed'],
            'warehouse_id' => $srow['warehouse_id'],
            ));
        }
    }

  // Check for insufficient product inventory levels.
  // Returns an error message if any product items cannot be filled.
  // You must call this before save().
  //
    public function checkInventory(&$prod)
    {
        $alertmsg = '';
        $insufficient = 0;
        $expiredlots = false;
        if (is_array($prod)) {
            foreach ($prod as $iter) {
                if (!empty($iter['billed'])) {
                    continue;
                }

                $drug_id   = $iter['drug_id'];
                $sale_id     = empty($iter['sale_id']) ? 0 : intval($iter['sale_id']); // present only if already saved
                $units     = empty($iter['units']) ? 1 : intval($iter['units']);
                $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse'];

                        // Deleting always works.
                if (!empty($iter['del'])) {
                    continue;
                }

                        // If the item is already in the database...
                if ($sale_id) {
                    $query = "SELECT ds.quantity, ds.inventory_id, di.on_hand, di.warehouse_id " .
                    "FROM drug_sales AS ds " .
                    "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " .
                    "WHERE ds.sale_id = ?";
                    $dirow = sqlQuery($query, array($sale_id));
                    // There's no inventory ID when this is a non-dispensible product (i.e. no inventory).
                    if (!empty($dirow['inventory_id'])) {
                        if ($warehouse_id && $warehouse_id != $dirow['warehouse_id']) {
                            // Changing warehouse so check inventory in the new warehouse.
                            // Nothing is updated by this call.
                            if (!sellDrug(
                                $drug_id,
                                $units,
                                0,
                                $this->pid,
                                $this->encounter,
                                0,
                                $this->visit_date,
                                '',
                                $warehouse_id,
                                true,
                                $expiredlots
                            )) {
                                $insufficient = $drug_id;
                            }
                        } else {
                            if (($dirow['on_hand'] + $dirow['quantity'] - $units) < 0) {
                                $insufficient = $drug_id;
                            }
                        }
                    }
                } // Otherwise it's a new item...
                else {
                    // This only checks for sufficient inventory, nothing is updated.
                    if (!sellDrug(
                        $drug_id,
                        $units,
                        0,
                        $this->pid,
                        $this->encounter,
                        0,
                        $this->visit_date,
                        '',
                        $warehouse_id,
                        true,
                        $expiredlots
                    )) {
                        $insufficient = $drug_id;
                    }
                }
            } // end for
        }

        if ($insufficient) {
            $drow = sqlQuery("SELECT name FROM drugs WHERE drug_id = ?", array($insufficient));
            $alertmsg = xl('Insufficient inventory for product') . ' "' . $drow['name'] . '".';
            if ($expiredlots) {
                $alertmsg .= " " . xl('Check expiration dates.');
            }
        }

        return $alertmsg;
    }

  // Save posted data to the database. $bill and $prod are the incoming arrays of line items, with
  // key names corresponding to those generated by addServiceLineItem() and addProductLineItem().
  //
    public function save(
        &$bill,
        &$prod,
        $main_provid = null,
        $main_supid = null,
        $default_warehouse = null,
        $mark_as_closed = false
    ) {
        global $code_types;

        if (isset($main_provid) && $main_supid == $main_provid) {
            $main_supid = 0;
        }

        $copay_update = false;
        $update_session_id = '';
        $ct0  = ''; // takes the code type of the first fee type code type entry from the fee sheet, against which the copay is posted
        $cod0 = ''; // takes the code of the first fee type code type entry from the fee sheet, against which the copay is posted
        $mod0 = ''; // takes the modifier of the first fee type code type entry from the fee sheet, against which the copay is posted

        if (is_array($bill)) {
            foreach ($bill as $iter) {
                        // Skip disabled (billed) line items.
                if (!empty($iter['billed'])) {
                    continue;
                }

                $id        = $iter['id'];
                $code_type = $iter['code_type'];
                $code      = $iter['code'];
                $del       = !empty($iter['del']);
                $units     = empty($iter['units']) ? 1 : intval($iter['units']);
                $price     = empty($iter['price']) ? 0 : (0 + trim($iter['price']));
                $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel'];
                $revenue_code  = empty($iter['revenue_code']) ? '' : trim($iter['revenue_code']);
                $modifier  = empty($iter['mod']) ? '' : trim($iter['mod']);
                $justify   = empty($iter['justify'  ]) ? '' : trim($iter['justify']);
                $notecodes = empty($iter['notecodes']) ? '' : trim($iter['notecodes']);
                $provid    = empty($iter['provid'   ]) ? 0 : intval($iter['provid']);

                $fee       = sprintf('%01.2f', $price * $units);

                if (!$cod0 && $code_types[$code_type]['fee'] == 1) {
                    $mod0 = $modifier;
                    $cod0 = $code;
                    $ct0  = $code_type;
                }

                if ($code_type == 'COPAY') {
                    if ($fee < 0) {
                        $fee = $fee * -1;
                    }

                    if (!$id) {
                        // adding new copay from fee sheet into ar_session and ar_activity tables
                        $session_id = idSqlStatement(
                            "INSERT INTO ar_session " .
                            "(payer_id, user_id, pay_total, payment_type, description, patient_id, payment_method, " .
                            "adjustment_code, post_to_date) " .
                            "VALUES ('0',?,?,'patient','COPAY',?,'','patient_payment',now())",
                            array($_SESSION['authId'], $fee, $this->pid)
                        );
                        sqlBeginTrans();
                        $sequence_no = sqlQuery("SELECT IFNULL(MAX(sequence_no),0) + 1 AS increment FROM ar_activity WHERE " .
                          "pid = ? AND encounter = ?", array($this->pid, $this->encounter));
                        SqlStatement(
                            "INSERT INTO ar_activity (pid, encounter, sequence_no, code_type, code, modifier, " .
                            "payer_type, post_time, post_user, session_id, " .
                            "pay_amount, account_code) VALUES (?,?,?,?,?,?,0,now(),?,?,?,'PCP')",
                            array($this->pid, $this->encounter, $sequence_no['increment'], $ct0, $cod0, $mod0,
                            $_SESSION['authId'],
                            $session_id,
                            $fee)
                        );
                        sqlCommitTrans();
                    } else {
                        // editing copay saved to ar_session and ar_activity
                        $session_id = $id;
                        $res_amount = sqlQuery(
                            "SELECT pay_amount FROM ar_activity WHERE pid=? AND encounter=? AND session_id=?",
                            array($this->pid, $this->encounter, $session_id)
                        );
                        if ($fee != $res_amount['pay_amount']) {
                              sqlStatement(
                                  "UPDATE ar_session SET user_id=?,pay_total=?,modified_time=now(),post_to_date=now() WHERE session_id=?",
                                  array($_SESSION['authId'], $fee, $session_id)
                              );
                                  sqlStatement(
                                      "UPDATE ar_activity SET code_type=?, code=?, modifier=?, post_user=?, post_time=now(),".
                                      "pay_amount=?, modified_time=now() WHERE pid=? AND encounter=? AND account_code='PCP' AND session_id=?",
                                      array($ct0, $cod0, $mod0, $_SESSION['authId'], $fee, $this->pid, $this->encounter, $session_id)
                                  );
                        }
                    }

                    if (!$cod0) {
                        $copay_update = true;
                        $update_session_id = $session_id;
                    }

                    continue;
                }

                        # Code to create justification for all codes based on first justification
                if ($GLOBALS['replicate_justification'] == '1') {
                    if ($justify != '') {
                         $autojustify = $justify;
                    }
                }

                if (($GLOBALS['replicate_justification'] == '1') && ($justify == '') && check_is_code_type_justify($code_type)) {
                    $justify =  $autojustify;
                }

                if ($justify) {
                    $justify = str_replace(',', ':', $justify) . ':';
                }

                $auth      = "1";

                $ndc_info = '';
                if (!empty($iter['ndcnum'])) {
                    $ndc_info = 'N4' . trim($iter['ndcnum']) . '   ' . $iter['ndcuom'] .
                    trim($iter['ndcqty']);
                }

                        // If the item is already in the database...
                if ($id) {
                    if ($del) {
                        $this->logFSMessage(xl('Service deleted'));
                        deleteBilling($id);
                    } else {
                        $tmp = sqlQuery(
                            "SELECT * FROM billing WHERE id = ? AND (billed = 0 or billed is NULL) AND activity = 1",
                            array($id)
                        );
                        if (!empty($tmp)) {
                              $tmparr = array('code' => $code, 'authorized' => $auth);
                            if (isset($iter['units'    ])) {
                                $tmparr['units'      ] = $units;
                            }

                            if (isset($iter['price'    ])) {
                                $tmparr['fee'        ] = $fee;
                            }

                            if (isset($iter['pricelevel'])) {
                                $tmparr['pricelevel'] = $pricelevel;
                            }

                            if (isset($iter['mod'      ])) {
                                $tmparr['modifier'   ] = $modifier;
                            }

                            if (isset($iter['provid'   ])) {
                                $tmparr['provider_id'] = $provid;
                            }

                            if (isset($iter['ndcnum'   ])) {
                                $tmparr['ndc_info'   ] = $ndc_info;
                            }

                            if (isset($iter['justify'  ])) {
                                $tmparr['justify'    ] = $justify;
                            }

                            if (isset($iter['notecodes'])) {
                                $tmparr['notecodes'  ] = $notecodes;
                            }

                            if (isset($iter['revenue_code'])) {
                                $tmparr['revenue_code'] = $revenue_code;
                            }

                            foreach ($tmparr as $key => $value) {
                                if ($tmp[$key] != $value) {
                                    if ('fee'         == $key) {
                                        $this->logFSMessage(xl('Price changed'));
                                    }

                                    if ('units'       == $key) {
                                        $this->logFSMessage(xl('Quantity changed'));
                                    }

                                    if ('provider_id' == $key) {
                                        $this->logFSMessage(xl('Service provider changed'));
                                    }

                                    sqlStatement("UPDATE billing SET `$key` = ? WHERE id = ?", array($value, $id));
                                }
                            }
                        }
                    }
                } // Otherwise it's a new item...
                else if (!$del) {
                    $this->logFSMessage(xl('Service added'));
                    $code_text = lookup_code_descriptions($code_type.":".$code);
                    addBilling(
                        $this->encounter,
                        $code_type,
                        $code,
                        $code_text,
                        $this->pid,
                        $auth,
                        $provid,
                        $modifier,
                        $units,
                        $fee,
                        $ndc_info,
                        $justify,
                        0,
                        $notecodes,
                        $pricelevel,
                        $revenue_code
                    );
                }
            } // end for
        }

        // if modifier is not inserted during loop update the record using the first
        // non-empty modifier and code
        if ($copay_update == true && $update_session_id != '' && $mod0 != '') {
            sqlStatement(
                "UPDATE ar_activity SET code_type = ?, code = ?, modifier = ?".
                " WHERE pid = ? AND encounter = ? AND account_code = 'PCP' AND session_id = ?",
                array($ct0, $cod0, $mod0, $this->pid, $this->encounter, $update_session_id)
            );
        }

        // Doing similarly to the above but for products.
        if (is_array($prod)) {
            foreach ($prod as $iter) {
                        // Skip disabled (billed) line items.
                if (!empty($iter['billed'])) {
                    continue;
                }

                $drug_id   = $iter['drug_id'];
                $selector  = empty($iter['selector']) ? '' : $iter['selector'];
                $sale_id   = $iter['sale_id']; // present only if already saved
                $units     = max(1, intval(trim($iter['units'])));
                $price     = empty($iter['price']) ? 0 : (0 + trim($iter['price']));
                $pricelevel = empty($iter['pricelevel']) ? '' : $iter['pricelevel'];
                $fee       = sprintf('%01.2f', $price * $units);
                $del       = !empty($iter['del']);
                $rxid      = 0;
                $warehouse_id = empty($iter['warehouse']) ? '' : $iter['warehouse'];
                $somechange = false;

                        // If the item is already in the database...
                if ($sale_id) {
                    $tmprow = sqlQuery("SELECT ds.prescription_id, ds.quantity, ds.inventory_id, ds.fee, " .
                    "ds.sale_date, di.warehouse_id " .
                    "FROM drug_sales AS ds " .
                    "LEFT JOIN drug_inventory AS di ON di.inventory_id = ds.inventory_id " .
                    "WHERE ds.sale_id = ?", array($sale_id));
                    $rxid = 0 + $tmprow['prescription_id'];
                    if ($del) {
                        if (!empty($tmprow)) {
                            // Delete this sale and reverse its inventory update.
                            $this->logFSMessage(xl('Product deleted'));
                            sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id));
                            if (!empty($tmprow['inventory_id'])) {
                                sqlStatement(
                                    "UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?",
                                    array($tmprow['quantity'], $tmprow['inventory_id'])
                                );
                            }
                        }

                        if ($rxid) {
                              sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid));
                        }
                    } else {
                          // Modify the sale and adjust inventory accordingly.
                        if (!empty($tmprow)) {
                            foreach (array(
                              'quantity'    => $units,
                              'fee'         => $fee,
                              'pricelevel'  => $pricelevel,
                              'selector'    => $selector,
                              'sale_date'   => $this->visit_date,
                            ) as $key => $value) {
                                if ($tmprow[$key] != $value) {
                                                  $somechange = true;
                                    if ('fee'        == $key) {
                                        $this->logFSMessage(xl('Price changed'));
                                    }

                                    if ('pricelevel' == $key) {
                                        $this->logFSMessage(xl('Price level changed'));
                                    }

                                    if ('selector'   == $key) {
                                        $this->logFSMessage(xl('Template selector changed'));
                                    }

                                    if ('quantity'   == $key) {
                                        $this->logFSMessage(xl('Quantity changed'));
                                    }

                                                  sqlStatement(
                                                      "UPDATE drug_sales SET `$key` = ? WHERE sale_id = ?",
                                                      array($value, $sale_id)
                                                  );
                                    if ($key == 'quantity' && $tmprow['inventory_id']) {
                                                                            sqlStatement(
                                                                                "UPDATE drug_inventory SET on_hand = on_hand - ? WHERE inventory_id = ?",
                                                                                array($units - $tmprow['quantity'], $tmprow['inventory_id'])
                                                                            );
                                    }
                                }
                            }

                            if ($tmprow['inventory_id'] && $warehouse_id && $warehouse_id != $tmprow['warehouse_id']) {
                                // Changing warehouse.  Requires deleting and re-adding the sale.
                                // Not setting $somechange because this alone does not affect a prescription.
                                $this->logFSMessage(xl('Warehouse changed'));
                                sqlStatement("DELETE FROM drug_sales WHERE sale_id = ?", array($sale_id));
                                sqlStatement(
                                    "UPDATE drug_inventory SET on_hand = on_hand + ? WHERE inventory_id = ?",
                                    array($units, $tmprow['inventory_id'])
                                );
                                $tmpnull = null;
                                $sale_id = sellDrug(
                                    $drug_id,
                                    $units,
                                    $fee,
                                    $this->pid,
                                    $this->encounter,
                                    (empty($iter['rx']) ? 0 : $rxid),
                                    $this->visit_date,
                                    '',
                                    $warehouse_id,
                                    false,
                                    $tmpnull,
                                    $pricelevel,
                                    $selector
                                );
                            }
                        }

                          // Delete Rx if $rxid and flag not set.
                        if ($GLOBALS['gbl_auto_create_rx'] && $rxid && empty($iter['rx'])) {
                            sqlStatement("UPDATE drug_sales SET prescription_id = 0 WHERE sale_id = ?", array($sale_id));
                            sqlStatement("DELETE FROM prescriptions WHERE id = ?", array($rxid));
                        }
                    }
                } // Otherwise it's a new item...
                else if (! $del) {
                    $somechange = true;
                    $this->logFSMessage(xl('Product added'));
                    $tmpnull = null;
                    $sale_id = sellDrug(
                        $drug_id,
                        $units,
                        $fee,
                        $this->pid,
                        $this->encounter,
                        0,
                        $this->visit_date,
                        '',
                        $warehouse_id,
                        false,
                        $tmpnull,
                        $pricelevel,
                        $selector
                    );
                    if (!$sale_id) {
                        die(xlt("Insufficient inventory for product ID") . " \"" . text($drug_id) . "\".");
                    }
                }

                        // If a prescription applies, create or update it.
                if (!empty($iter['rx']) && !$del && ($somechange || empty($rxid))) {
                    // If an active rx already exists for this drug and date we will
                    // replace it, otherwise we'll make a new one.
                    if (empty($rxid)) {
                        $rxid = '';
                    }

                    // Get default drug attributes; prefer the template with the matching selector.
                    $drow = sqlQuery(
                        "SELECT dt.*, " .
                        "d.name, d.form, d.size, d.unit, d.route, d.substitute " .
                        "FROM drugs AS d, drug_templates AS dt WHERE " .
                        "d.drug_id = ? AND dt.drug_id = d.drug_id " .
                        "ORDER BY (dt.selector = ?) DESC, dt.quantity, dt.dosage, dt.selector LIMIT 1",
                        array($drug_id, $selector)
                    );
                    if (!empty($drow)) {
                            $rxobj = new Prescription($rxid);
                            $rxobj->set_patient_id($this->pid);
                            $rxobj->set_provider_id(isset($main_provid) ? $main_provid : $this->provider_id);
                            $rxobj->set_drug_id($drug_id);
                            $rxobj->set_quantity($units);
                            $rxobj->set_per_refill($units);
                            $rxobj->set_start_date_y(substr($this->visit_date, 0, 4));
                            $rxobj->set_start_date_m(substr($this->visit_date, 5, 2));
                            $rxobj->set_start_date_d(substr($this->visit_date, 8, 2));
                            $rxobj->set_date_added($this->visit_date);
                            // Remaining attributes are the drug and template defaults.
                            $rxobj->set_drug($drow['name']);
                            $rxobj->set_unit($drow['unit']);
                            $rxobj->set_dosage($drow['dosage']);
                            $rxobj->set_form($drow['form']);
                            $rxobj->set_refills($drow['refills']);
                            $rxobj->set_size($drow['size']);
                            $rxobj->set_route($drow['route']);
                            $rxobj->set_interval($drow['period']);
                            $rxobj->set_substitute($drow['substitute']);
                            //
                            $rxobj->persist();
                            // Set drug_sales.prescription_id to $rxobj->get_id().
                            $oldrxid = $rxid;
                            $rxid = 0 + $rxobj->get_id();
                        if ($rxid != $oldrxid) {
                            sqlStatement(
                                "UPDATE drug_sales SET prescription_id = ? WHERE sale_id = ?",
                                array($rxid, $sale_id)
                            );
                        }
                    }
                }
            } // end for
        }

        // Set default and/or supervising provider for the encounter.
        if (isset($main_provid) && $main_provid != $this->provider_id) {
            $this->logFSMessage(xl('Default provider changed'));
            sqlStatement(
                "UPDATE form_encounter SET provider_id = ? WHERE pid = ? AND encounter = ?",
                array($main_provid, $this->pid, $this->encounter)
            );
            $this->provider_id = $main_provid;
        }

        if (isset($main_supid) && $main_supid != $this->supervisor_id) {
            sqlStatement(
                "UPDATE form_encounter SET supervisor_id = ? WHERE pid = ? AND encounter = ?",
                array($main_supid, $this->pid, $this->encounter)
            );
            $this->supervisor_id = $main_supid;
        }

        // Save-and-Close is currently specific to Family Planning but might be more
        // generally useful.  It provides the ability to mark an encounter as billed
        // directly from the Fee Sheet, if there are no charges.
        if ($mark_as_closed) {
            $tmp1 = sqlQuery(
                "SELECT SUM(ABS(fee)) AS sum FROM drug_sales WHERE " .
                "pid = ? AND encounter = ? AND billed = 0",
                array($this->pid, $this->encounter)
            );
            $tmp2 = sqlQuery(
                "SELECT SUM(ABS(fee)) AS sum FROM billing WHERE " .
                "pid = ? AND encounter = ? AND billed = 0 AND activity = 1",
                array($this->pid, $this->encounter)
            );
            if ($tmp1['sum'] + $tmp2['sum'] == 0) {
                  sqlStatement(
                      "update drug_sales SET billed = 1 WHERE " .
                      "pid = ? AND encounter = ? AND billed = 0",
                      array($this->pid, $this->encounter)
                  );
                    sqlStatement(
                        "UPDATE billing SET billed = 1, bill_date = NOW() WHERE " .
                        "pid = ? AND encounter = ? AND billed = 0 AND activity = 1",
                        array($this->pid, $this->encounter)
                    );
            } else {
                  // Would be good to display an error message here... they clicked
                  // Save and Close but the close could not be done.  However the
                  // framework does not provide an easy way to do that.
            }
        }
    }

  // Call this after save() for Family Planning implementations.
  // It checks the contraception form, or makes a new one if $newmauser is set.
  // Returns 0 unless user intervention is required to fix a missing or incorrect form,
  // and in that case the return value is an existing form ID, or -1 if none.

  // Returns FALSE if user intervention is required to fix a missing or incorrect form.
  //
    public function doContraceptionForm($ippfconmeth = null, $newmauser = null, $main_provid = 0)
    {
        if (!empty($ippfconmeth)) {
            $csrow = sqlQuery(
                "SELECT f.form_id, ld.field_value FROM forms AS f " .
                "LEFT JOIN lbf_data AS ld ON ld.form_id = f.form_id AND ld.field_id = 'newmethod' " .
                "WHERE " .
                "f.pid = ? AND f.encounter = ? AND " .
                "f.formdir = 'LBFccicon' AND f.deleted = 0 " .
                "ORDER BY f.form_id DESC LIMIT 1",
                array($this->pid, $this->encounter)
            );
            if (isset($newmauser)) {
                  // pastmodern is 0 iff new to modern contraception
                  $pastmodern = $newmauser == '2' ? 0 : 1;
                if ($newmauser == '2') {
                    $newmauser = '1';
                }

                  // Add contraception form but only if it does not already exist
                  // (if it does, must be 2 users working on the visit concurrently).
                if (empty($csrow)) {
                    $newid = $this->insert_lbf_item(0, 'newmauser', $newmauser);
                    $this->insert_lbf_item($newid, 'newmethod', "IPPFCM:$ippfconmeth");
                    $this->insert_lbf_item($newid, 'pastmodern', $pastmodern);
                    // Do we care about a service-specific provider here?
                    $this->insert_lbf_item($newid, 'provider', $main_provid);
                    addForm($this->encounter, 'Contraception', $newid, 'LBFccicon', $this->pid, $GLOBALS['userauthorized']);
                }
            } else if (empty($csrow) || $csrow['field_value'] != "IPPFCM:$ippfconmeth") {
                  // Contraceptive method does not match what is in an existing Contraception
                  // form for this visit, or there is no such form. User intervention is needed.
                  return empty($csrow) ? -1 : intval($csrow['form_id']);
            }
        }

        return 0;
    }

  // Get price level from patient demographics.
  //
    public function getPriceLevel()
    {
        return $this->patient_pricelevel;
    }

  // Update price level in patient demographics if it's changed.
  //
    public function updatePriceLevel($pricelevel)
    {
        if (!empty($pricelevel)) {
            if ($this->patient_pricelevel != $pricelevel) {
                $this->logFSMessage(xl('Price level changed'));
                sqlStatement(
                    "UPDATE patient_data SET pricelevel = ? WHERE pid = ?",
                    array($pricelevel, $this->pid)
                );
                $this->patient_pricelevel = $pricelevel;
            }
        }
    }

  // Create JSON string representing code type, code and selector.
  // This can be a checkbox value for parsing when the checkbox is clicked.
  // As a side effect note if the code is already selected in the Fee Sheet.
  //
    public function genCodeSelectorValue($codes)
    {
        global $code_types;
        list($codetype, $code, $selector) = explode(':', $codes);
        if ($codetype == 'PROD') {
            $crow = sqlQuery(
                "SELECT sale_id " .
                "FROM drug_sales WHERE pid = ? AND encounter = ? AND drug_id = ? " .
                "LIMIT 1",
                array($this->pid, $this->encounter, $code)
            );
            $this->code_is_in_fee_sheet = !empty($crow['sale_id']);
            $cbarray = array($codetype, $code, $selector);
        } else {
            $crow = sqlQuery(
                "SELECT c.id AS code_id, b.id " .
                "FROM codes AS c " .
                "LEFT JOIN billing AS b ON b.pid = ? AND b.encounter = ? AND b.code_type = ? AND b.code = c.code AND b.activity = 1 " .
                "WHERE c.code_type = ? AND c.code = ? LIMIT 1",
                array($this->pid, $this->encounter, $codetype, $code_types[$codetype]['id'], $code)
            );
            $this->code_is_in_fee_sheet = !empty($crow['id']);
            $cbarray = array($codetype, $code);
        }

        $cbval = json_encode($cbarray);
        return $cbval;
    }
}
